/* eslint-disable @typescript-eslint/ban-types */
import { Reactive, ISetter, reactive, isReactive } from './reactive';
import { effect, isSignal, Signal } from './singals';
import { get, getSequence, parsePath, safeCall } from './utils';

type StateCreator<T> = (set: ISetter<T>, peek: () => T) => T;

class StateManager {
    private stateMap = new Map<string, Reactive<any>>();

    setState(stateId: string, state: any) {
        if (this.stateMap.has(stateId)) {
            stateId = `${stateId}_${getSequence()}`;
            console.warn(`%cState ${stateId} already exists, use new stateId: ${stateId}`, 'color: red');
        }

        this.stateMap.set(stateId, state);
    }

    getState(stateId: string) {
        return this.stateMap.get(stateId);
    }

    removeState(stateId: string) {
        this.stateMap.delete(stateId);
    }
}

export const stateManager = new StateManager();

const STATE_ID_KEY = Symbol('state_id');

// 是状态
export function isState(obj: any) {
    return obj && STATE_ID_KEY in obj;
}

// 获取状态的id
export function getStateId(obj: any) {
    return obj[STATE_ID_KEY];
}

export function create<T extends object>(initializer: StateCreator<T> | T, id?: string): Reactive<T>;
export function create<T extends object>(target: T, id?: string): Reactive<T> {
    const _stateId = id ?? `$state_${getSequence()}`;
    const isGlobal = !!id

    const state = reactive<T>(target as any)

    if (isGlobal) {
        stateManager.setState(_stateId, state)
    }
    state[STATE_ID_KEY] = _stateId;
    return state
}

type WatchCallback<T> = (current: T, prev: T) => void
type WatchSource<T> = string | Reactive<T> | Signal<T> | (() => T | Signal<T>)

export function watch<T>(source: WatchSource<T> | WatchSource<T>[], cb: WatchCallback<T | T[]>, _state?: Reactive<any>, self?: any): () => void {
    let getter: () => any;
    const pathGetter = (path: string) => {
        const pi = parsePath(path);
        if (!pi) {
            throw new Error(`Invalid path: ${path}`);
        }

        const state = pi.stateId === "*" ? _state : stateManager.getState(pi.stateId);
        if (!state) {
            throw new Error(`State [${pi.stateId}] not found`);
        }
        return get(state, pi.subpath);
    }

    if (isSignal(source)) {
        getter = () => source.value;
    } else if (isReactive(source)) {
        getter = () => shallowTraverse(source)
    } else if (typeof source === 'string') {
        getter = () => pathGetter(source);
    } else if (Array.isArray(source)) {
        getter = () => (source as any[]).map((s)=> {
            if (isSignal(s)) {
                return s.value;
            } else if (isReactive(s)) {
                return shallowTraverse(s);
            } else if (typeof s === 'string') {
                return pathGetter(s);
            } else if (typeof s === 'function') {
                return (s as any)();
            }
        });
    } else if (typeof source === 'function') {
        getter = () => (source as any)();
    } else {
        getter = () => {};
    }

    const _notifyDatas: [any, any][] = [];
    let _pendingNotify = false;
    const batchNotify = (data: [any, any]) => {
        _notifyDatas.push(data);

        if (!_pendingNotify) {
            _pendingNotify = true;
            Promise.resolve().then(() => {
                _pendingNotify = false;
                const _datas = _notifyDatas.splice(0, _notifyDatas.length);

                const first = _datas.shift();
                let last = _datas.pop();
                if (!last) {
                    last = first;
                }

                const [current, prev] = [last[0], first[1]];
                safeCall(cb, self, current, prev);
            })
        }
    }

    let oldValue: any;
    let isFirst = true;
    return effect(function() {
        try {
            const newValue = getter();
            if (!isFirst) {
                batchNotify([newValue, oldValue]);
            }
            isFirst = false;
            oldValue = newValue;
        } catch (e) {
            console.error(e);
            this.dispose();
        }
    })
}

function shallowTraverse(value: any) {
    const skip = ['peek', 'set', 'subscribe'];
    const result = {};
    
    for (const key in value) {
        if (skip.indexOf(key) !== -1) {
            continue;
        }

        const _value = value[key];
        if (typeof _value === 'function') {
            continue;
        }

        result[key] = _value;
    }

    return result;
}
